const { danger, fail, warn, message } = require("danger");
const fs = require("fs");
const vm = require("vm");
const tty = require("tty");

const information = (msg, file, line) => {
  message(msg, file, line);
};
const failure = (msg, file, line) => {
  fail(msg, file, line);
};
const warning = (msg, file, line) => {
  warn(msg, file, line);
};
/** @param {'warn'|'fail'|'info'} level */
const logger = (level, args) => {
  const { message, file = undefined, line = undefined } = args;
  switch (level) {
    case "fail":
      failure(message, file, line);
      break;
    case "warn":
      warning(message, file, line);
      break;
    case "info":
      information(message, file, line);
      break;
    default:
      console.warn(message, file, line);
  }
};

process.on("uncaughtException", (e) => {});
process.on("unhandledRejection", (e) => {});

/** Same as above library
 * @see https://github.com/sindresorhus/yoctocolors/blob/main/index.js */
class Format {
  static hasColors = tty.WriteStream.prototype.hasColors();
  /** Intentionally not using template literal for performance. */
  static format = (startCode = 0, endCode = 0) =>
    Format.hasColors
      ? (string) =>
          "\u001B[" + startCode + "m" + string + "\u001B[" + endCode + "m"
      : (string) => string;

  static reset = Format.format(0, 0);
  static Color = {
    black: Format.format(30, 39),
    red: Format.format(31, 39),
    green: Format.format(32, 39),
    yellow: Format.format(33, 39),
    blue: Format.format(34, 39),
    magenta: Format.format(35, 39),
    cyan: Format.format(36, 39),
    white: Format.format(37, 39),
    gray: Format.format(90, 39),
  };
  static Style = {
    bold: Format.format(1, 22),
    dim: Format.format(2, 22),
    italic: Format.format(3, 23),
    underline: Format.format(4, 24),
    overline: Format.format(53, 55),
    inverse: Format.format(7, 27),
    hidden: Format.format(8, 28),
    strikethrough: Format.format(9, 29),
  };
  static toString(obj) {
    if (typeof obj !== "object") return JSON.stringify(obj);
    const properties = Object.getOwnPropertyNames(obj);
    const rest = {};
    for (let key of properties) {
      if (typeof obj[key] === "function") {
        rest[key] = "Function";
      } else {
        rest[key] = String(obj[key]);
      }
      if (rest[key] === "[object Object]")
        rest[key] = Format.toString(obj[key]);
    }
    return JSON.stringify(rest);
  }
}

class File extends String {
  constructor(str, git_type) {
    super(str);
    this.git_type = git_type;
  }
  git_type = "";
  static get [Symbol.species]() {
    return String;
  }
}

const commitFiles = [
  ...danger.git.created_files.map((file) => new File(file, "created")),
  ...danger.git.deleted_files.map((file) => new File(file, "deleted")),
  ...danger.git.modified_files.map((file) => new File(file, "modified")),
];

const RegExps = {
  lockfile: /(.*)package-lock\.json/i,
  packageJson: /(.*)package\.json/i,
  alphaBeta: /alpha|beta/i,
};

/** 比较 repo 下面对应的*同级目录*是否被同时修改
 * @param {String} rootDir 对应同级目录的根路径，如 hera 下的 `src/pages`
 * @param {String[]} ranges 对应同级目录形式，如 hera 下的 `DT/FL/FS/LS`
 * @returns {String[]} 被修改的目录路径
 */
const dirChangedInSameMR = (rootDir, ranges) => {
  const hasRangeChanged = ranges.map((part) => {
    const path = `${rootDir}/${part}/`;
    return !!commitFiles.some((file) => file.startsWith(path));
  });

  return hasRangeChanged
    .map((range, idx) => (range ? `${rootDir}/${ranges[idx]}` : undefined))
    .filter(Boolean);
};

const formatMessages = (summary = "", mdContent = []) => {
  return `
<details><summary>${summary}</summary>

${mdContent.join("\r\n")}
</details>`;
};

/** 检查commitFile是否满足对应正则或str
 *
 * @param {string|File} commitFile
 */
const fileMatcher = (commitFile = "", reg$Str = "") => {
  if (Object.prototype.toString.call(reg$Str) === "[object RegExp]") {
    return commitFile.match(reg$Str);
  }

  return commitFile.toString() === reg$Str;
};

const lockfileDiff = (
  packageLockDiff,
  registry = "",
  antiParams = [],
  semver = ""
) => {
  function isValidRegistry(foundUrl) {
    return (foundUrl || "").startsWith(registry);
  }
  function isValidParams(foundUrl) {
    if (antiParams.length < 1) return true;
    const params = new URLSearchParams(new URL(foundUrl).search);
    for (let param of antiParams) {
      if (params.has(param)) {
        return false;
      }
    }
    return true;
  }
  function semverValidator(changed) {
    if (changed.length <= 0) return;
    let major = [],
      minor = [],
      patch = [];
    for (let { key, before, after } of changed) {
      if (!String(before).startsWith(after.substring(0, after.indexOf(".")))) {
        major.push(key);
      } else if (
        !String(before).startsWith(after.substring(0, after.lastIndexOf(".")))
      ) {
        minor.push(key);
      } else {
        patch.push(key);
      }
    }
    let hint = "";
    //don't break
    switch (semver) {
      case "patch":
        hint = "小";
        major.push(...patch);
        patch = [];
      case "minor":
        hint = "中";
        major.push(...minor);
        minor = [];
      case "major":
        hint = "大";
        break;
    }
    if (major.length > 0 && hint !== "") {
      failure(`依赖不允许${hint}版本修改！请检查以下包：${major.join()}`);
      major = [];
    }
    if (major.length > 0 || minor.length > 0 || patch.length > 0) {
      warning(
        `修改的包: **Major**: ${major.join()}, **Minor**: ${minor.join()}, **Patch**: ${patch.join()}`
      );
    }
  }
  if (packageLockDiff.dependencies) {
    const dpd = packageLockDiff.dependencies;
    if (dpd.removed?.length > 0) {
      warning(`移除的包: ${dpd.removed.join()}`);
    }
    if (dpd.added?.length > 0) {
      warning(`添加的包: ${dpd.added.join()}`);
    }
    let changed = [],
      entries = [],
      failedRegistry = [],
      failedParams = [],
      mixChanged = [];
    if (!dpd["before"]) {
      // 只修改
      entries = Object.entries(dpd);
      for (let [key, { before, after }] of entries) {
        if (!before || !after) {
          mixChanged.push(key);
          continue;
        }
        if (before.version !== after.version) {
          changed.push({
            key,
            before: before.version,
            after: after.version,
          });
        }
        if (!isValidRegistry(after.resolved)) {
          failedRegistry.push(key);
        }
        if (!isValidParams(after.resolved)) {
          failedParams.push(key);
        }
      }
    } else {
      entries = Object.entries(dpd["after"]);
      // 删除或添加+修改
      for (let [key, { version, resolved }] of entries) {
        if (!isValidRegistry(resolved)) {
          failedRegistry.push(key);
        }
        if (!isValidParams(resolved)) {
          failedParams.push(key);
        }
        if (dpd.added.includes(key)) {
          continue;
        }
        if (dpd["before"][key].version !== version) {
          changed.push({
            key,
            before: dpd.before[key].version,
            after: version,
          });
        }
      }
    }
    if (failedRegistry.length > 0) {
      failure(
        `lockfile 的 resolved 字段允许且仅允许 \`${registry}\`, 请检查以下包: ${failedRegistry.join()}`
      );
    }
    if (failedParams.length > 0) {
      failure(
        `lockfile 的 resolved 字段不允许 \`[${antiParams.join()}]\`, 请检查以下包: ${failedParams.join()} `
      );
    }
    if (mixChanged.length > 0) {
      failure(
        `lockfile 包版本没变，但是其依赖变化了，请检查以下包: ${mixChanged.join()}`
      );
    }
    semverValidator(changed);
  }
};

class DangerWorker {
  constructor(repo) {
    this.repo = repo;
  }
  static mrInfoCheck() {
    if (!danger?.gitlab?.mr) return;

    const { description, title } = danger.gitlab.mr || {};

    if (typeof title === 'string' && title.toLocaleUpperCase().startsWith("WIP")) {
      warning(`WIP 的 MR。**跳过检查**。

Merge 前请重命名为其他内容`);
      process.exit(0);
    }

    if (description && description.length < 10) {
      warning("这个 MR 没有必要的描述(如果有 Warn，请提示为什么)");
    }
  }
  static branchCheck(config) {
    const target = danger.gitlab.mr.target_branch;
    const expects = config?.targets || [];
    if (expects.length > 0 && expects.indexOf(target) < 0) {
      return false;
    }

    console.log(
      Format.Color.green(`检查${danger.gitlab.mr.source_branch} -> ${target}`)
    );
    return true;
  }
  printer(config) {
    Object.entries(config || {}).forEach(([related, cfg]) => {
      if (["created", "deleted", "modified"].includes(related) && cfg) {
        const files = danger.git[related + "_files"];
        if (Array.isArray(files) && files.length > 0) {
          logger(cfg.level | "info", {
            message: formatMessages(
              cfg.summary || related,
              files.map((i) => `- ${i}`)
            ),
          });
        }
      }
    });

    return true;
  }
  lockfileCheck(config, commitFile) {
    if (!config) return true;

    const matched = commitFile.match(RegExps.lockfile);
    if (!matched) {
      return false;
    }

    const hasPackageChanges = !!commitFiles.some(
      (modified) => modified.toString() === `${matched[1]}package.json`
    );
    if (!hasPackageChanges) {
      failure(`\`${commitFile}\` 有变动，但是没有对应的 package.json 变更`);
    }

    danger.git
      .JSONDiffForFile(commitFile.toString())
      .then((packageLockDiff) => {
        const lockVersion =
          packageLockDiff?.lockfileVersion?.added ||
          packageLockDiff?.lockfileVersion?.after;
        if (
          config.lockVersion &&
          lockVersion &&
          config.lockVersion !== lockVersion
        ) {
          failure(
            `package-lock.json 版本必须为 v${config.lockVersion} 但是发现版本为 v${lockVersion}`
          );
        }
        if (config.registry && typeof config.registry !== "boolean") {
          lockfileDiff(
            packageLockDiff,
            config.registry.url || "https://",
            config.registry.antiParams || [],
            commitFile.git_type === "created" ? "" : config.lockSemver
          );
        }
      });
  }
  packageJsonCheck(config, commitFile) {
    if (!Array.isArray(config)) return true;

    let match = false;
    config?.forEach((cfg) => {
      const { locked = [], noBeta, pkgJsonPath, custom, logChange } = cfg || {};
      if (pkgJsonPath) {
        match = fileMatcher(commitFile, pkgJsonPath);
      } else {
        match = commitFile.match(RegExps.packageJson);
      }
      if (!match) return;

      const changelogReg = new RegExp(
        `^${commitFile.substring(
          0,
          commitFile.lastIndexOf("/")
        )}(?:.*\/)*CHANGELOG\.md$`,
        "iu"
      );
      const changelogFile = commitFiles.filter((file) =>
        changelogReg.test(file)
      )[0];

      danger.git.JSONDiffForFile(commitFile.toString()).then((packageDiff) => {
        const pkgVersion =
          packageDiff?.version?.after || packageDiff?.version?.added;
        if (typeof custom === "function") {
          custom(
            { file: { name: commitFile, diff: packageDiff } },
            { logger, formatMessages }
          );
          return;
        }
        if (logChange && pkgVersion) {
          if (changelogFile) {
            danger.git
              .diffForFile(changelogFile.toString())
              .then((changelogDiff) => {
                console.log(changelogDiff);
                const diff = changelogDiff?.after || "";
                if (diff.indexOf(pkgVersion) < 0) {
                  warning(
                    `\`${commitFile}\`版本变更且\`${changelogFile}\`变了，但相关版本号未记录`
                  );
                }
              });
          } else {
            warning(
              `\`${commitFile}\`版本有变更，但没有相关\`CHANGELOG.md\`变`
            );
          }
        }
        if (locked.length > 0 || typeof noBeta === "boolean") {
          const dpd = Object.assign(
            packageDiff.dependencies || {},
            packageDiff.devDependencies || {}
          );
          Object.entries(dpd.after || {}).forEach(([pck, newVer]) => {
            const trimmedVer = String(newVer).trim();
            locked.forEach((value) => {
              if (
                value?.name === pck &&
                commitFile.git_type !== "created" &&
                !!trimmedVer &&
                value?.version !== trimmedVer
              ) {
                failure(`不要升级 \`${value.name}\`, 因为: ${value.reason}`);
              }
            });
            if (noBeta)
              if (trimmedVer.match(RegExps.alphaBeta)) {
                failure(`不要包含beta包 \`${pck}\``);
              }
          });
        }
      });
    });
  }
  rangeCheck(configs = []) {
    if (!Array.isArray(configs)) return true;

    configs.forEach((group) => {
      const modified = dirChangedInSameMR(group.rootDir, group.workDirs);
      if (modified.length > 0) {
        logger(group.level || "warn", {
          message: formatMessages(
            group.summary || `Modified in \`${group.rootDir}\``,
            modified.map((item) => "- `" + item + "`")
          ),
        });
      }
    });

    return true;
  }
  protectedFilesCheck(config = [], commitFile) {
    if (!Array.isArray(config)) return true;
    if (commitFile.git_type === "created") return false;

    config?.forEach((cfg) => {
      if (fileMatcher(commitFile, !!cfg.file ? cfg.file : cfg)) {
        const msg = !!cfg.reason
          ? `\`${commitFile}\`, 因为: ${cfg.reason}`
          : commitFile;
        logger(cfg.level || "warn", {
          message:
            "不要" + commitFile.git_type === "deleted" ? "删除" : "修改" + msg,
        });
      }
    });
  }
  run() {
    if (!DangerWorker.branchCheck(this.repo?.branchCheck)) return;
    DangerWorker.mrInfoCheck();

    const properties = Object.getOwnPropertyNames(DangerWorker.prototype);
    const escaped = ["constructor", "run"];
    const workProperties = properties
      .map((property) => {
        if (escaped.includes(property)) return;
        if (typeof this[property] === "function") {
          const args = this.repo[property];
          if (!args) {
            console.log(`${Format.Color.green(property)} passed`);
            return;
          }
          console.log(
            Format.Style.bold(Format.Color.yellow(property)),
            ":",
            Format.Color.magenta(Format.toString(args))
          );
          return { property, args };
        }
      })
      .filter(Boolean);

    for (let { property, args } of workProperties) {
      if (escaped.includes(property)) continue;
      fileLoop: for (let i = 0; i < commitFiles.length; i++) {
        const commitFile = commitFiles[i];
        const result = this[property].call(this, args, commitFile);
        if (result) {
          escaped.push(property);
          break fileLoop;
        }
      }
    }
  }
}

(function main() {
  const slug = danger.gitlab.metadata.repoSlug;
  const [base, project] = slug.split("/");
  const replacedSlug = encodeURIComponent(`${project}@${base}.js`);
  fetch(
    `https://gitlab.qtrade.com.cn/api/v4/projects/452/repository/files/danger%2Fconfigs%2F${replacedSlug}/raw?ref=master`,
    {
      headers: {
        "PRIVATE-TOKEN": process.env.GITLAB_API_TOKEN,
      },
    }
  )
    .then((resp) => resp.text())
    .catch((err) => {
      console.error(err);
      failure("加载 config 超时");
    })
    .then((data) => {
      console.log(
        Format.Color.cyan(
          "已加载的配置: " + Format.Style.bold(`configs/${replacedSlug}`)
        )
      );
      const script = new vm.Script(data, {});
      const sandbox = {
        loadConfig: function (config) {
          if (!config) {
            return;
          }
          const worker = new DangerWorker(config);
          worker.run();
        },
      };
      vm.createContext(sandbox);
      script.runInContext(sandbox);
    });
})();
